# 好比特套利策略
# 分为两个脚本，一个是好比特下单脚本，一个是比特币平衡脚本
# 当前脚本为 好比特下单 脚本

import markets
import config
import math
import random
import time
from concurrent.futures import ThreadPoolExecutor
from operator import attrgetter

exchanges = []
init_btc = 0.0  # 程序运行前，比特币数量，在程序运行过程中，数量一般保持不变

total_cny = 0.0  # 当前的人民币数量和比特币数量
total_btc = 0.0


def init_exchanges():
    exchange_names = config.exchanges
    global exchanges  # 哪里需要使用全局变量，哪里就声明一下
    for exchange_name in exchange_names:
        try:
            exec('import markets.' + exchange_name.lower())
            exchange = eval('markets.' + exchange_name.lower() + '.' + exchange_name + '()')
            exchanges.append(exchange)
            datetime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(time.time())))
            print("%s: Init exchange %s." % (datetime, exchange_name))
        except Exception as e:
            print("%s exchange name is invalid, please check config.py" % exchange_name)
            print(e)


# decimal表示浮点数值，number表示保留小数位数；number只能为1，2，3或者4
def retainDigiNum(decimal, number):  # 保留N位小数，不四舍五入；最好别用round函数，有精度误差的问题，会产生特别长的浮点数
    if number == 1:
        return int(decimal * 10) / 10
    if number == 2:
        return int(decimal * 100) / 100
    if number == 3:
        return int(decimal * 1000) / 1000
    if number == 4:
        return int(decimal * 10000) / 10000
    if number == 5:
        return int(decimal * 100000) / 100000
    if number == 6:
        return int(decimal * 1000000) / 1000000


def cancelAll():  # 不撤销好比特的订单
    global exchanges
    for exchange in exchanges:
        if exchange.__class__.__name__ == 'Haobtc':
            continue
        exchange.cancel_all()
    datetime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(time.time())))
    print("%s: Cancel all orders except Haobtc!" % datetime)


def updateCurrentDepth(exclude=[]):  # 参数为当前操作不更新的市场列表
    global exchanges
    try:
        threadpool = ThreadPoolExecutor(max_workers=10)
        for exchange in exchanges:
            if exchange.__class__.__name__ in exclude:
                continue
            threadpool.submit(exchange.update_depth)
        threadpool.shutdown(wait=True)
    except Exception as e:
        # 若深度更新失败，将所有交易所深度信息置为0
        for exchange in exchanges:
            exchange.bid = 0.0
            exchange.bid_volume = 0.0
            exchange.ask = 0.0
            exchange.ask_volume = 0.0
        print('UpdateCurrentDepth Failed!' + str(e))


def updateCurrentAsset(exclude=[]):
    global exchanges
    try:
        threadpool = ThreadPoolExecutor(max_workers=10)
        for exchange in exchanges:
            if exchange.__class__.__name__ in exclude:
                continue
            threadpool.submit(exchange.update_accountInfo)
        threadpool.shutdown(wait=True)
    except Exception as e:
        # 若资产更新失败，将所有交易所的资产信息都置为0
        for exchange in exchanges:
            exchange.btc_free = 0.0
            exchange.btc_frozen = 0.0
            exchange.cny_free = 0.0
            exchange.cny_frozen = 0.0
        print('UpdateCurrentAsset Failed!' + str(e))

    global total_cny, total_btc
    total_btc = 0.0
    total_cny = 0.0
    for exchange in exchanges:
        total_cny += exchange.cny_free
        total_cny += exchange.cny_frozen
        total_btc += exchange.btc_free
        total_btc += exchange.btc_frozen


def isNomal():  # 判断交易所资产更新是否正常
    flag = True
    for exchange in exchanges:
        if (exchange.cny_free == 0) and (exchange.cny_frozen == 0) and (exchange.btc_free == 0) and \
                (exchange.btc_frozen == 0):
            flag = False
            break
    return flag


def isDepthNomal():  # 判断除好比特外的交易所深度是否正常
    flag = True
    for exchange in exchanges:
        if exchange.__class__.__name__ == 'Haobtc':
            continue
        if (exchange.ask == 0) or (exchange.bid == 0):
            flag = False
            break
    return flag


def handleHaobtc():  # 处理Haobtc的成交情况
    global exchanges
    # 作为参照时，ask, bid都不能为0；如果为零，跳过
    ask_min = 0
    ask_max = 0
    bid_min = 0
    bid_max = 0
    HaobtcPendingBidMaxVolume = 0
    HaobtcPendingAskMaxVolume = 0

    print("----------Haobtc----------")

    updateCurrentAsset(["Haobtc"])  # 因为只是获取市场参考价格和深度，所以不更新Haobtc资产和深度
    updateCurrentDepth(["Haobtc"])

    if isDepthNomal():  # 除好比特以外的市场深度正常时，才继续下去；否则，不对好比特套利进行操作
        # 其他交易所的ask价即为平衡时的买入价，所以按ask价从低到高排列
        # 依次遍历，如果交易所的可用人民币大于1000元，则为ask最优价；取最后一个为次优价；若没有最优价，次优价也为最优价
        IsGetOptimal = False  # 是否得到最优标志
        for exchange in sorted(exchanges, key=attrgetter('ask'), reverse=False):
            if exchange.__class__.__name__ == 'Haobtc':
                continue
            if (exchange.ask == 0) or (exchange.bid == 0):
                continue
            if (exchange.cny_free > config.MinAmountOnce * exchange.ask) and (not IsGetOptimal):
                ask_min = exchange.ask
                HaobtcPendingAskMaxVolume = retainDigiNum(exchange.cny_free / exchange.ask, 4)
                IsGetOptimal = True
            ask_max = exchange.ask
        if not IsGetOptimal:  # 如果没有取得最优价，则使用次优价，这里指的是较高的ask
            ask_min = ask_max

        # 其他交易所的bid价即为平衡时的卖出价，所以按bid价从高到低排列
        # 依次遍历，如果交易所的可用比特币资产大于1000元，则为bid最优价；取最后一个为次优价；若没有最优价，次优价也为最优价
        IsGetOptimal = False
        for exchange in sorted(exchanges, key=attrgetter('bid'), reverse=True):
            if exchange.__class__.__name__ == 'Haobtc':
                continue
            if (exchange.ask == 0) or (exchange.bid == 0):
                continue
            if (exchange.btc_free > config.MinAmountOnce) and (not IsGetOptimal):
                bid_max = exchange.bid
                HaobtcPendingBidMaxVolume = retainDigiNum(exchange.btc_free, 4)
                IsGetOptimal = True
            bid_min = exchange.bid
        if not IsGetOptimal:  # 如果没有取得最优价，则使用次优价，这里指的是较低的bid
            bid_max = bid_min

        amountHaobtcBidPending = 0  # 统计Haobtc买单上的比特币累积数量
        amountHaobtcAskPending = 0  # 统计Haobtc卖单上的比特币累积数量
        for exchange in exchanges:
            if exchange.__class__.__name__ == 'Haobtc':
                exchange.update_depth()
                # 如果Haobtc更新深度失败，即bid或ask为零，则不进行撤单、下单操作
                if (exchange.bid == 0) or (exchange.ask == 0):
                    break

                # 撤销不在下单范围的订单；撤销可能产生亏损的单；撤销在下单范围内的，但不是最优情况的订单
                orders = exchange.get_orders()
                if orders:
                    for order in orders:
                        if order['status'] != 'All deal':
                            if order['type'] == 'BUY':
                                if (bid_max != 0) and \
                                        (float(order['price']) * (1 - config.makerRate * config.ORDERWARNINGRATE) >= bid_max):
                                    # 买单没有利润，撤单
                                    exchange.cancel_order(order['order_id'])
                                    continue
                                if float(order['price']) < exchange.bid - config.OrderRetainScope + 1:
                                    # 买单不在挂单范围内，撤单
                                    exchange.cancel_order(order['order_id'])
                                    continue
                                if float(order['price']) >= exchange.bid - config.OrderRetainScope + 1:
                                    # 买单在保留范围内，但非最优
                                    if float(order['price']) <= (exchange.ask-1)-config.OrderValidScope:  # 在有效范围外面
                                        # 临界值使用次优价来计算，减少撤单概率
                                        if float(order['price']) <= (exchange.ask - 1) - config.OrderValidScope \
                                                < int(bid_min / (1 - config.makerRate * config.ORDERWARNINGRATE)):
                                            # 如果订单价格 <= 当前买一价 - 有效范围 < 临界值，撤单
                                            exchange.cancel_order(order['order_id'])
                                            continue
                                        elif float(order['price']) < int(
                                                        bid_min / (1 - config.makerRate * config.ORDERWARNINGRATE)) \
                                                <= (exchange.ask - 1) - config.OrderValidScope:
                                            # 如果订单价格 < 临界值向下取整 <= 当前买一价 - 有效范围，撤单
                                            exchange.cancel_order(order['order_id'])
                                            continue
                                    else:  # 在有效范围里面
                                        # 使用模拟退火算法来算当前撤单的概率
                                        if float(order['price']) \
                                                < (exchange.ask-1) \
                                                < int(bid_min / (1 - config.makerRate * config.ORDERWARNINGRATE)):
                                            if (1 - math.exp((float(order['price']) - (exchange.ask-1)
                                                              ) / config.OrderValidScope / 2)) > random.random():
                                                exchange.cancel_order(order['order_id'])
                                                continue
                                        if float(float(order['price'])) \
                                                < int(bid_min / (1 - config.makerRate * config.ORDERWARNINGRATE)) \
                                                <= (exchange.ask-1):
                                            if (1 - math.exp((float(order['price']) - int(
                                                        bid_min / (1 - config.makerRate * config.ORDERWARNINGRATE))
                                                              ) / config.OrderValidScope / 2)) > random.random():
                                                exchange.cancel_order(order['order_id'])
                                                continue

                                # 统计好比特当前已下买单的比特币数量。PS：只统计订单成交时的成本价在两个交易所之间的情况，
                                # 即Ok和火币差价大的情况
                                if bid_min < float(order['price']) * (1 - config.makerRate * config.ORDERWARNINGRATE) < bid_max:
                                    amountHaobtcBidPending += float(order['remainsize'])
                                    if amountHaobtcBidPending > HaobtcPendingBidMaxVolume:
                                        exchange.cancel_order(order['order_id'])
                                        amountHaobtcBidPending -= float(order['remainsize'])
                                        continue

                            elif order['type'] == 'SELL':
                                if (ask_min != 0) and \
                                        (float(order['price']) * (1 + config.makerRate * config.ORDERWARNINGRATE) <= ask_min):
                                    # 卖单没有利润，撤单
                                    exchange.cancel_order(order['order_id'])
                                    continue
                                if float(order['price']) > exchange.ask + config.OrderRetainScope - 1:
                                    # 卖单不在挂单范围内，撤单
                                    exchange.cancel_order(order['order_id'])
                                    continue
                                if float(order['price']) <= exchange.ask + config.OrderRetainScope - 1:  # 卖单在保留范围内，但非最优
                                    if float(order['price']) >= (exchange.bid+1)+config.OrderValidScope:  # 在有效范围外面
                                        # 临界值用次优价来算，减少撤单概率
                                        if math.ceil(ask_max / (1 + config.makerRate * config.ORDERWARNINGRATE)) \
                                                < (exchange.bid + 1) + config.OrderValidScope <= float(order['price']):
                                            # 临界值 < 当前卖一价+有效范围 <= 订单价格，撤单
                                            exchange.cancel_order(order['order_id'])
                                            continue
                                        elif (exchange.bid + 1) + config.OrderValidScope \
                                                <= math.ceil(ask_max / (1 + config.makerRate * config.ORDERWARNINGRATE)) \
                                                < float(order['price']):
                                            # 当前卖一价+有效范围 <= 临界值向上取整 < 订单价格，撤单
                                            exchange.cancel_order(order['order_id'])
                                            continue
                                    else:  # 在有效范围里面
                                        # 使用模拟退火算法来算当前撤单的概率
                                        if math.ceil(ask_max / (1 + config.makerRate * config.ORDERWARNINGRATE)) \
                                                < exchange.bid+1 \
                                                < float(order['price']):
                                            if (1-math.exp((exchange.bid+1-float(
                                                    order['price']))/config.OrderValidScope/2)) > random.random():
                                                exchange.cancel_order(order['order_id'])
                                                continue
                                        if exchange.bid+1 \
                                                <= math.ceil(ask_max / (1 + config.makerRate * config.ORDERWARNINGRATE)) \
                                                < float(order['price']):
                                            if (1-math.exp((math.ceil(ask_max/(
                                                        1+config.makerRate*config.ORDERWARNINGRATE)) - float(order['price'])
                                                            )/config.OrderValidScope/2)) > random.random():
                                                exchange.cancel_order(order['order_id'])
                                                continue

                                # 统计好比特当前已下卖单的比特币数量。PS：只统计订单成交时的成本价在两个交易所之间的情况，
                                # 即Ok和火币差价大的情况
                                if ask_min < float(order['price']) * (1 + config.makerRate * config.ORDERWARNINGRATE) < ask_max:
                                    amountHaobtcAskPending += float(order['remainsize'])
                                    if amountHaobtcAskPending > HaobtcPendingAskMaxVolume:
                                        exchange.cancel_order(order['order_id'])
                                        amountHaobtcAskPending -= float(order['remainsize'])
                                        continue

                # 撤销完好比特没利润或非最优的订单后，再更新好比特的资产信息
                exchange.update_accountInfo()

                # 主动套利
                if (bid_max != 0) and (exchange.cny_free > config.MaxAmountOnce*exchange.ask) \
                        and (bid_max - exchange.ask*(1 + exchange.fee / 100) > config.MinDiff):
                    # 最低差价为config.MinDiff元
                    # 买操作之后，将好比特的cny_free减去config.MaxAmountOnce对应的钱，避免再次更新资产导致的延迟
                    exchange.buy(int(exchange.ask), config.MaxAmountOnce)
                    exchange.cny_free -= config.MaxAmountOnce * exchange.ask
                if (ask_min != 0) and (exchange.btc_free > config.MaxAmountOnce) \
                        and (exchange.bid*(1 - exchange.fee / 100) - ask_min > config.MinDiff):
                    # 最低差价为config.MinDiff元
                    # 卖操作之后，将好比特的btc_free减去config.MaxAmountOnce的币，避免再次更新资产导致的延迟
                    exchange.sell(int(exchange.bid), config.MaxAmountOnce)
                    exchange.btc_free -= config.MaxAmountOnce

                # 被动套利
                # 用对盘计算临界值，跟最优价和次优价对比，以主动寻求机会
                # Haobtc下买单时的正常情况
                if (exchange.cny_free > config.MinAmountOnce * exchange.ask) and (bid_min != 0) \
                        and ((exchange.ask-1) * (1 - config.makerRate * config.ORDERWARNINGRATE) < bid_min):
                    exchange.buy_maker_only(int(exchange.ask-1), retainDigiNum(exchange.cny_free / (exchange.ask-1), 4))

                # Haobtc下卖单时的正常情况
                if (exchange.btc_free > config.MinAmountOnce) and (ask_max != 0) \
                        and ((exchange.bid+1) * (1 + config.makerRate * config.ORDERWARNINGRATE) > ask_max):
                    exchange.sell_maker_only(int(exchange.bid+1), retainDigiNum(exchange.btc_free, 4))

                # 其他市场差价过大时，Haobtc下买单存在最优价的情况
                if (exchange.cny_free > config.MinAmountOnce * exchange.ask) \
                        and (amountHaobtcBidPending < HaobtcPendingBidMaxVolume - config.MinAmountOnce) \
                        and (bid_max != 0) \
                        and (bid_min <= (exchange.ask-1) * (1 - config.makerRate * config.ORDERWARNINGRATE) < bid_max):
                    # 下在买一上的数量
                    amount_pend_in_firstprice = min(HaobtcPendingBidMaxVolume,
                                                    retainDigiNum(exchange.cny_free / (exchange.ask-1), 4),
                                                    retainDigiNum(HaobtcPendingBidMaxVolume - amountHaobtcBidPending,
                                                                  4))
                    # 剩余的数量，下在次优价格对应的临界值上
                    amount_pend_in_next_price = exchange.cny_free / (exchange.ask-1) - amount_pend_in_firstprice
                    # 下买单
                    exchange.buy_maker_only(int(exchange.ask-1), amount_pend_in_firstprice)
                    if amount_pend_in_next_price > config.MinAmountOnce:
                        exchange.buy_maker_only(int(bid_min / (1 - config.makerRate * config.ORDERWARNINGRATE)),
                                     retainDigiNum(amount_pend_in_next_price, 4))

                # 其他市场最优价格不能产生利润时
                # 下买单，没有利润，下在临界值向下取整
                if (exchange.cny_free > config.MinAmountOnce * exchange.ask) \
                        and (amountHaobtcBidPending < HaobtcPendingBidMaxVolume - config.MinAmountOnce) \
                        and (bid_max != 0) \
                        and (bid_max < (exchange.ask-1) * (1 - config.makerRate * config.ORDERWARNINGRATE)):
                    if int(bid_max / (1 - config.makerRate * config.ORDERWARNINGRATE)) > exchange.bid - config.OrderRetainScope:
                        exchange.buy_maker_only(int(bid_max / (1 - config.makerRate * config.ORDERWARNINGRATE)),
                                     min(HaobtcPendingBidMaxVolume,
                                         # 下全部空余资金
                                         retainDigiNum(
                                             exchange.cny_free / int(bid_max / (1 - config.makerRate * config.ORDERWARNINGRATE)),
                                             4),
                                         retainDigiNum(HaobtcPendingBidMaxVolume - amountHaobtcBidPending, 4)))

                # 其他市场差价过大时，Haobtc下卖单存在最优价的情况
                if (exchange.btc_free > config.MinAmountOnce) \
                        and (amountHaobtcAskPending < HaobtcPendingAskMaxVolume - config.MinAmountOnce) \
                        and (ask_max != 0) \
                        and (ask_min < (exchange.bid+1) * (1 + config.makerRate * config.ORDERWARNINGRATE) <= ask_max):
                    # 下在卖一上的数量
                    amount_pend_in_firstprice = min(HaobtcPendingAskMaxVolume,
                                                    retainDigiNum(exchange.btc_free, 4),
                                                    retainDigiNum(HaobtcPendingAskMaxVolume - amountHaobtcAskPending,
                                                                  4))
                    # 下在次优价格对应的临界值上的数量
                    amount_pend_in_next_price = exchange.btc_free - amount_pend_in_firstprice
                    # 下卖单
                    exchange.sell_maker_only(int(exchange.bid+1), retainDigiNum(amount_pend_in_firstprice, 4))
                    if amount_pend_in_next_price > config.MinAmountOnce:
                        exchange.sell_maker_only(math.ceil(ask_max / (1 + config.makerRate * config.ORDERWARNINGRATE)),
                                      retainDigiNum(amount_pend_in_next_price, 4))

                # 其他市场最优价格不能产生利润时
                # 下卖单，没有利润，下在临界值向上取整
                if (exchange.btc_free > config.MinAmountOnce) \
                        and (amountHaobtcAskPending < HaobtcPendingAskMaxVolume - config.MinAmountOnce) \
                        and (ask_max != 0) \
                        and ((exchange.bid+1) * (1 + config.makerRate * config.ORDERWARNINGRATE) < ask_min):
                    if math.ceil(ask_min / (1 + config.makerRate * config.ORDERWARNINGRATE))\
                            < exchange.ask + config.OrderRetainScope:
                        exchange.sell_maker_only(math.ceil(ask_min / (1 + config.makerRate * config.ORDERWARNINGRATE)),
                                      min(HaobtcPendingAskMaxVolume,
                                          # 下全部空余资金
                                          retainDigiNum(exchange.btc_free, 4),
                                          retainDigiNum(HaobtcPendingAskMaxVolume - amountHaobtcAskPending, 4)))

                datetime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(time.time())))
                print("%s: Pending Haobtc complete." % datetime)
                break
    else:
        datetime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(time.time())))
        print("%s: Pending Haobtc failed! Other exchanges' depth error!" % datetime)


def onTick():
    global total_cny
    handleHaobtc()


def main():
    global init_btc, total_btc, last_cny, total_cny, last_btc
    init_exchanges()
    cancelAll()
    while True:
        onTick()
        time.sleep(random.randint(4, int(config.TickInterval * 10)) / 10)


if __name__ == '__main__':
    main()
